﻿
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.Globalization;
using System.Threading;
using System.Web.Services.Protocols;

using Swaf;
using Swaf.BizObj;
using Swaf.BizRule;
using Swaf.Container;

using eCom;

using ScsCore.FedexRates;

namespace Fedex
{
	public class FedexRateBizRules : Syrinx.BaseBizRules
	{		
		public IBizObj getFedexShippingMethods(IApplication app, IRuleContext context, IBizObj order)
		{
			IBizObj deliveryOptions = app.bizObjMgr.create("eComDeliveryOptions");
			IBizObjList options = deliveryOptions.get("options") as IBizObjList;
			IBizObjList errors = deliveryOptions.get("errors") as IBizObjList;
			IBizObjList boxes = app.bizObjMgr.createList("ShippingBox");
			foreach (IBizObj orderItem in (IBizObjList)order.get("items"))
			{
				//For the first version, this code will only support products that are the shipping box 
				//(which should have dimensions,weight defined for them) or
				//products that have custom shipping boxes.  Right now we dont have the algorithm to calc
				//what boxes are needed for products that
				ShippingBoxOption option = (ShippingBoxOption)orderItem.get("product.shippingBoxOption");
				int quantity = (int)orderItem.get("quantity");
				for (int c = 0; c < quantity; c++)
				{
					switch (option)
					{
						case ShippingBoxOption.ProductIsShippingBox:
							IBizObj box = boxes.insertNew();
							box.put("id", new Swaf.BGuid.Guid());
							box.put("maxWeight", ((IBizObj)orderItem.get("product.weight")).Clone());
							box.put("dimensions", ((IBizObj)orderItem.get("product.dimensions")).Clone());
							break;
						case ShippingBoxOption.CustomShippingBoxes:
							FlexiMap m = new FlexiMap();
							m["productId"] = orderItem.get("product.id");
							IBizObjList customBoxes = app.appData.get("DA.eCom.GetProductSpecificBoxes", m) as IBizObjList;
							foreach (IBizObj custBox in customBoxes)
								boxes.insertNewMakeCopy(custBox);
							break;
						case ShippingBoxOption.ShipWithOtherProducts:
							break;
					}
				}
			}

			if (boxes.numElements > 0)
			{
				RateRequest request = createRateRequest(app);
				IBizObj warehouseFilter = app.appData.get("eComWarehouseContactFilter") as IBizObj;
				IResults rc = context.RuleGroup.execute("getContacts", DetailLevel.fullDetails, ActiveCompanyId, warehouseFilter);
				IBizObjList warehouses = rc.CallResults as IBizObjList;
				if (warehouses != null && warehouses.numElements > 0)
				{
					//For now we will just use the first warehouse in the list for all source shipping locations.
					//Later this should be expanded to calculate which warehouse products will ship from, which 
					//could actually change the number of shipments needed for a given order.
					IBizObj warehouse = warehouses[0];
					setupRequestAddresses(app, request, (IBizObj)warehouse.get("defaultAddress"), (IBizObj)order.get("orderContact.defaultAddress"));
					setupPaymentInfo(app, request);
					setupShipmentBoxes(app, request, boxes);

					RateService rateService = new RateService(); // Initialize the service
					if (!(bool)ActiveCompany.get("settings.fedex.useSandbox"))
						rateService.Url = "https://gateway.fedex.com:443/web-services";

					try
					{
						// This is the call to the web service passing in a RateRequest and returning a RateReply
						RateReply reply = rateService.getRates(request); // Service call
						//
						if (reply.HighestSeverity == NotificationSeverityType.SUCCESS || reply.HighestSeverity == NotificationSeverityType.NOTE || reply.HighestSeverity == NotificationSeverityType.WARNING) // check if the call was successful
						{
							FlexiMap m = new FlexiMap();
							m["name"] = "FedEx";
							IBizObj fedexProvider = app.appData.get("DA.eCom.GetDeliveryProviderByName", m) as IBizObj;
							foreach (RateReplyDetail rateDetail in reply.RateReplyDetails)
							{
								IBizObj op = options.insertNew();
								op.put("id", new Swaf.BGuid.Guid());
								op.put("provider", fedexProvider);
								op.put("deliveryMethodName", cleanupMethodName(rateDetail.ServiceType.ToString()));
								foreach (RatedShipmentDetail shipmentDetail in rateDetail.RatedShipmentDetails)
								{
									if (shipmentDetail.ShipmentRateDetail.RateType == ReturnedRateType.PAYOR_ACCOUNT)
									{
										op.put("shippingTotal", shipmentDetail.ShipmentRateDetail.TotalNetCharge.Amount);
										break;
									}
								}
							}
						}
						else
						{
							IBizObj error = errors.insertNew();
							FlexiMap m = new FlexiMap();
							error.put("errorMessageId", "Error-GeneralDeliveryFailure");
							m.put("code", reply.Notifications[0].Code);
							m.put("longMessage", reply.Notifications[0].Message);
							m.put("source", reply.Notifications[0].Source);
							m.put("severity", reply.Notifications[0].Severity.ToString());
							error.put("errorInfo", m);
						}
					}
					catch (Exception e)
					{
						IBizObj error = errors.insertNew();
						FlexiMap m = new FlexiMap();
						error.put("errorMessageId", "Error-GeneralDeliveryFailure");
						m.put("code", "ServiceUnavailable");
						m.put("longMessage", "FedEx RateService is unavailable");
						m.put("source", e.Message);
						error.put("errorInfo", m);
					}
				}
			}

			return deliveryOptions;
		}

		public IBizObjList calculateBoxesForFedexShipping(IApplication app, IRuleContext context, IBizObj salesOrder)
		{
			IBizObjList boxes = app.bizObjMgr.createList("ShippingBox");

			return boxes;
		}


		protected RateRequest createRateRequest(IApplication app)
		{
			RateRequest request = new RateRequest();
	
			//
			request.WebAuthenticationDetail = new WebAuthenticationDetail();
			request.WebAuthenticationDetail.UserCredential = new WebAuthenticationCredential();
			request.ClientDetail = new ClientDetail();

			request.WebAuthenticationDetail.UserCredential.Key = (string)ActiveCompany.get("settings.fedex.accountKey");
			request.WebAuthenticationDetail.UserCredential.Password = (string)ActiveCompany.get("settings.fedex.password");
			request.ClientDetail.AccountNumber = (string)ActiveCompany.get("settings.fedex.accountNum");
			request.ClientDetail.MeterNumber = (string)ActiveCompany.get("settings.fedex.meterNum");
			//
			request.TransactionDetail = new TransactionDetail();
			request.TransactionDetail.CustomerTransactionId = "Rates Available Service"; // This is a reference field for the customer.  Any value can be used and will be provided in the response.
			//
			request.Version = new VersionId(); // WSDL version information, value is automatically set from wsdl            
			// 
			request.ReturnTransitAndCommit = true;
			request.ReturnTransitAndCommitSpecified = true;
			request.CarrierCodes = new CarrierCodeType[2];
			// Insert the Carriers you would like to see the rates for
			request.CarrierCodes[0] = CarrierCodeType.FDXE;
			request.CarrierCodes[1] = CarrierCodeType.FDXG;
			request.RequestedShipment = new RequestedShipment();


			return request;
		}


		protected void setupRequestAddresses(IApplication app, RateRequest request, IBizObj sourceAddr, IBizObj destAddr)
		{
			request.RequestedShipment.Shipper = new Party();
			// Origin information
			request.RequestedShipment.Shipper.Address = new Address();
			request.RequestedShipment.Shipper.Address.StreetLines = new string[1] { (string)sourceAddr.get("street1") };
			request.RequestedShipment.Shipper.Address.City = (string)sourceAddr.get("city");
			request.RequestedShipment.Shipper.Address.StateOrProvinceCode = (string)sourceAddr.get("stateProvince");
			request.RequestedShipment.Shipper.Address.PostalCode = (string)sourceAddr.get("zipCode");
			request.RequestedShipment.Shipper.Address.CountryCode = (string)sourceAddr.get("country.regionCode");
			//
			request.RequestedShipment.Recipient = new Party();
			// Destination Information
			request.RequestedShipment.Recipient.Address = new Address();
			request.RequestedShipment.Recipient.Address.StreetLines = new string[1] { (string)destAddr.get("street1") };
			request.RequestedShipment.Recipient.Address.City = (string)destAddr.get("city");
			string countryCode = (string)destAddr.get("country.regionCode");
			if("US".CompareTo(countryCode)==0)
				request.RequestedShipment.Recipient.Address.StateOrProvinceCode = (string)destAddr.get("stateProvince");
			request.RequestedShipment.Recipient.Address.PostalCode = (string)destAddr.get("zipCode");
			request.RequestedShipment.Recipient.Address.CountryCode = countryCode;
            request.RequestedShipment.Recipient.Address.Residential = true;
            request.RequestedShipment.Recipient.Address.ResidentialSpecified = true;
		}

		protected void setupPaymentInfo(IApplication app, RateRequest request)
		{
			request.RequestedShipment.ShippingChargesPayment = new Payment(); // Payment Information
			request.RequestedShipment.ShippingChargesPayment.PaymentType = PaymentType.SENDER; // Payment options are RECIPIENT, SENDER, THIRD_PARTY
			request.RequestedShipment.ShippingChargesPayment.PaymentTypeSpecified = true;
			request.RequestedShipment.ShippingChargesPayment.Payor = new Payor();
			request.RequestedShipment.ShippingChargesPayment.Payor.AccountNumber = (string)ActiveCompany.get("settings.fedex.accountNum");
			request.RequestedShipment.DropoffType = DropoffType.REGULAR_PICKUP; //Drop off types are BUSINESS_SERVICE_CENTER, DROP_BOX, REGULAR_PICKUP, REQUEST_COURIER, STATION

			//TODO: Allow fedex option to control if shipping is insured for amount of product in boxes.
			request.RequestedShipment.TotalInsuredValue = new Money();
			request.RequestedShipment.TotalInsuredValue.Amount = 0;
			request.RequestedShipment.TotalInsuredValue.Currency = "USD";
			//TODO: Does the ship time affect rate? It could if there is a planned increase that starts before the
			//shipping is actually triggered.
			request.RequestedShipment.ShipTimestamp = DateTime.Now; // Shipping date and time
			request.RequestedShipment.ShipTimestampSpecified = true;
			request.RequestedShipment.RateRequestTypes = new RateRequestType[1];
			//request.RequestedShipment.RateRequestTypes[0] = RateRequestType.ACCOUNT;
			request.RequestedShipment.RateRequestTypes[0] = RateRequestType.LIST;
		}

		protected void setupShipmentBoxes(IApplication app, RateRequest request, IBizObjList boxes)
		{
			request.RequestedShipment.PackageCount = boxes.numElements.ToString();
			request.RequestedShipment.PackageDetail = RequestedPackageDetailType.INDIVIDUAL_PACKAGES;
			request.RequestedShipment.RequestedPackages = new RequestedPackage[boxes.numElements];
			int pos = 0;
			foreach (IBizObj box in boxes)
			{
				request.RequestedShipment.RequestedPackages[pos] = new RequestedPackage();
				request.RequestedShipment.RequestedPackages[pos].Weight = new Weight(); // package weight
				request.RequestedShipment.RequestedPackages[pos].Weight.Units = getWeightUnitFromName((string)box.get("maxWeight.unit"));
				request.RequestedShipment.RequestedPackages[pos].Weight.Value = (decimal)box.get("maxWeight.amount");
				
				//TODO: The dimensions bizobj supports different measurement types being used for each length in the dimension
				//Fedex does not.  That means this code will need to be beefed up to support that
				request.RequestedShipment.RequestedPackages[pos].Dimensions = new Dimensions(); // package dimensions
				request.RequestedShipment.RequestedPackages[pos].Dimensions.Length = box.get("dimensions.length.amount").ToString();
				request.RequestedShipment.RequestedPackages[pos].Dimensions.Width = box.get("dimensions.width.amount").ToString();
				request.RequestedShipment.RequestedPackages[pos].Dimensions.Height = box.get("dimensions.depth.amount").ToString();
				request.RequestedShipment.RequestedPackages[pos].Dimensions.Units = getLinearUnitFromName(box.get("dimensions.length.unit").ToString());

				//TODO: Allow fedex option to control if shipping is insured for amount of product in boxes.
				request.RequestedShipment.RequestedPackages[pos].InsuredValue = new Money(); // insured value
				request.RequestedShipment.RequestedPackages[pos].InsuredValue.Amount = 0;
				request.RequestedShipment.RequestedPackages[pos].InsuredValue.Currency = "USD";
				pos++;
			}
		}

		//TODO: The following two methods should be beefed up to deal with things like "hundreds of an pound" and other odd
		//measurements, which would affect the value used too.  For now, this means all product and box measures must be
		//specified in lb/kg or in/cm.
		protected WeightUnits getWeightUnitFromName(string weightName)
		{
			weightName = weightName.ToLower();
			if (weightName.StartsWith("lb"))
				return WeightUnits.LB;
			return WeightUnits.KG;
		}
		protected LinearUnits getLinearUnitFromName(string name)
		{
			name = name.ToLower();
			if (name.StartsWith("in"))
				return LinearUnits.IN;
			else
				return LinearUnits.CM;
		}

		protected string cleanupMethodName(string name)
		{
			name = name.Replace('_', ' ').Replace("FEDEX ", "").ToLower();
			name = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(name);
			return name;
		}
	}
}
